package com.wuhao.code.check.gotohandler

import com.aegis.mybatis.xmlless.annotations.UseEntityConvert
import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler
import com.intellij.lang.xml.XMLLanguage
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.vfs.findPsiFile
import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl
import com.intellij.psi.*
import com.intellij.psi.util.parentOfType
import com.intellij.psi.util.parentsOfType
import com.intellij.psi.xml.XmlDocument
import com.intellij.psi.xml.XmlTag
import com.intellij.psi.xml.XmlText
import com.intellij.psi.xml.XmlToken
import com.wuhao.code.check.cachedPosterity
import com.wuhao.code.check.completion.XmlReferenceContributor
import com.wuhao.code.check.completion.isAfterFromOrJoin
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.*

/**
 * @param el
 * @return
 */
fun getNamespace(el: PsiElement): String? {
  val doc = el.parentOfType<XmlDocument>() ?: return null
  val root = doc.rootTag ?: return null
  if (root.name != "mapper") {
    return null
  }
  return root.getAttributeValue("namespace")
}

/**
 * @param el
 * @return
 */
fun isConvertEntityMethod(el: XmlToken): Boolean {
  val id = getNamespace(el) ?: return false
  val cls = JavaPsiFacade.getInstance(el.project).findClass(id, el.resolveScope) ?: return false
  val methodTag = el.parentsOfType<XmlTag>().find {
    it.getAttributeValue("id") != null
  } ?: return false
  val methodName = if (methodTag.name == "sql") {
    val doc = el.parentOfType<XmlDocument>() ?: return false
    val root = doc.rootTag ?: return false
    XmlReferenceContributor.findQuoteTag(root, methodTag.getAttributeValue("id")!!)
        ?.getAttributeValue("id")
  } else {
    methodTag.getAttributeValue("id")
  } ?: return false
  val method = cls.methods.find { it.name == methodName } ?: return false
  return method.hasAnnotation(UseEntityConvert::class.java.name)
}

/**
 * @param el
 * @param cursorPosition
 * @return
 */
fun resolveProperty(el: XmlToken, cursorPosition: Int): Result? {
  val realNameAndPos = getRealColumnName(el)
  if (cursorPosition > 0 && (cursorPosition < realNameAndPos.second || cursorPosition >= realNameAndPos.third)) {
    return null
  }
  val realName = realNameAndPos.first
  val parts = realName.split(".")
  if (parts.size != 2) {
    return null
  }
  val alias = parts[0]
  val clazz = findClassByAlias(alias, el) ?: return Result(null, null, realNameAndPos.first, alias, parts[1])
  val field = clazz.allFields.find {
    it.name == parts[1]
  }
  if (field != null) {
    return Result(clazz, field, realNameAndPos.first, alias, parts[1])
  }
  return Result(clazz, null, realNameAndPos.first, alias, parts[1])
}

/**
 * @param el
 * @return
 */
internal fun getRealColumnName(el: PsiElement): Triple<String, Int, Int> {
  val text = el.text
  val startPos = el.startOffset
  val endPos = el.endOffset
  val a = "\\w+\\.\\w+".toRegex().find(text) ?: return Triple(text, startPos, endPos)
  val group = a.groups.firstOrNull() ?: return Triple(text, startPos, endPos)
  return Triple(
      group.value,
      startPos + group.range.first,
      startPos + group.range.last + 1
  )
}

/**
 * @param alias
 * @param el
 * @return
 */
fun findClassByAlias(alias: String, el: XmlToken): PsiClass? {
  val tag = el.parents.find { it is XmlTag && it.getAttributeValue("id") != null } ?: return null
  tag.children.filterIsInstance<XmlText>().forEach { text ->
    val aliasToken = text.allChildren.find {
      it is XmlToken && it.text in listOf(alias, "$alias,")
    }
    if (aliasToken != null) {
      val prev = aliasToken.getPrevSiblingIgnoringWhitespace() ?: return null
      if (prev is XmlToken) {
        return resolveEntityClass(prev).firstOrNull()
      }
    }
  }
  return null
}

/**
 * @param el
 * @return
 */
fun resolveEntityClass(
    el: XmlToken
): Array<PsiClass> {
  if (!isAfterFromOrJoin(el)) {
    return arrayOf()
  }
  val directory = PsiManager.getInstance(el.project).findDirectory(el.project.baseDir)?.virtualFile
  if (directory !is VirtualDirectoryImpl) {
    return arrayOf()
  }
  val files = directory.cachedPosterity.filter { it.name.endsWith(".java") || it.name.endsWith(".kt") }
  val classes: MutableList<PsiClass> = ArrayList()
  for (file in files) {
    val psiFile = file.findPsiFile(el.project)
    if (psiFile is PsiJavaFile) {
      classes.addAll(psiFile.classes)
    } else if (psiFile is KtFile) {
      classes.addAll(psiFile.classes)
    }
  }
  val matched = classes.filter { it.name == el.text }
  return matched.toTypedArray()
}

/**
 * Created by 吴昊 on 2017/7/18.
 * @author 吴昊
 * @since 1.3.8
 */
class MyBatisGotoDeclarationHandler : GotoDeclarationHandler {

  override fun getActionText(context: DataContext): String? {
    return null
  }

  override fun getGotoDeclarationTargets(el: PsiElement?, cursorPosition: Int, editor: Editor?): Array<out PsiElement> {
    if (el == null || el.language !is XMLLanguage) {
      return arrayOf()
    }
    if (el !is XmlToken || el.parent !is XmlText) {
      return arrayOf()
    }
    if (!isConvertEntityMethod(el)) {
      return arrayOf()
    }
    val text = el.text
    return if (text.contains(".")) {
      val property = resolveProperty(el, cursorPosition)
      if (property?.field != null) {
        return arrayOf(property.field)
      }
      return arrayOf()
    } else {
      resolveEntityClass(el)
    }
  }

}

/**
 * @author 吴昊
 * @date 2024/02/01
 * @version 1.0
 * @since v1.0.0
 */
class Result(
    val clazz: PsiClass?,
    val field: PsiField?,
    val text: String,
    val alias: String,
    val fieldName: String
)
